/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * $Header:$
 */

package org.pentaho.di.trans.steps.webservices.wsdl;

import org.w3c.dom.Element;

import javax.xml.namespace.QName;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

/*
 This looks a little scary, but isn't so bad.  Pretty much all that needs to be done here is to
 parse a NAMED complex type in the wsdl's types section.  We really only care about the <element>'s
 contained within the complex type.  The semantics don't matter (choice, sequence, etc).  The end result
 should be a ComplexType object which contains only elements.  This type will be used during client
 type registration.
 */

/**
 * A ComplexType contians a map of the elementName -> elementXmlType of all the elements in a
 * named complex type.
 */
public final class ComplexType implements java.io.Serializable {

    private static final long serialVersionUID = 1L;
    private final HashMap<String, QName> _elements = new HashMap<String, QName>();
    private final List<String> _elementNames = new ArrayList<String>();
    private final String _name;
    private WsdlTypes _wsdlTypes;

    /**
     * Create a new complex type for the specified element.
     *
     * @param type     DOM element of the complex type.
     * @param wsdlTypes Namespace resolver instance.
     */
    ComplexType(Element type, WsdlTypes wsdlTypes) {

        _name = type.getAttribute("name");
        _wsdlTypes = wsdlTypes;

        // annotation?, (simpleContent | complexContent
        // | ((group | all | choice | sequence)?, (attribute | attributeGroup)*, anyAttribute?))
        Element child;
        if ((child = DomUtils.getChildElementByName(type, "simpleContent")) != null) {
            processSimpleContent(child);
        }
        else if ((child = DomUtils.getChildElementByName(type, "complexContent")) != null) {
            processComplexContent(child);
        }
        else if ((child = DomUtils.getChildElementByName(type, "group")) != null) {
            processGroup(child);
        }
        else if ((child = DomUtils.getChildElementByName(type, "all")) != null) {
            processAll(child);
        }
        else if ((child = DomUtils.getChildElementByName(type, "choice")) != null) {
            processChoice(child);
        }
        else if ((child = DomUtils.getChildElementByName(type, "sequence")) != null) {
            processSequence(child);
        }

        // release the resolver, we don't need it after the parse is complete
        _wsdlTypes = null;
    }

    /**
     * Get the complex type name.
     *
     * @return String containing name of complex type.
     */
    public String getName() {
        return _name;
    }

    /**
     * Given the name of an element contained within the complex type, get its xml type.
     *
     * @param elementName Name of element contained within complex type.
     * @return Xmltype of the element or null if element can not be found in the complex type.
     */
    public QName getElementType(String elementName) {
        return (QName) _elements.get(elementName.toLowerCase());
    }

    /**
     * Get the set of all element names contained in this complex type.
     *
     * @return Set.
     */
    public List<String> getElementNames() {
        return _elementNames;
    }

    /* ---- Private Methods --- */

    /**
     * Process an 'all' element.
     *
     * @param all
     */
    private void processAll(Element all) {
        // annotation?, element*
        List<Element> elements = DomUtils.getChildElementsByName(all, "element");
        for (Iterator<Element> itr = elements.iterator(); itr.hasNext();) {
            processElement((Element) itr.next());
        }
    }

    /**
     * Process an 'any' element.
     *
     * @param any
     */
    private void processAny(Element any) {
        // annotation?
        // *** noop ***
    }

//    private void processAttribute(Element attribute) {
//        //  <complexType name="ArrayOf_xsd_short">
//        //   <complexContent>
//        //    <restriction base="soapenc:Array">
//        //     <attribute ref="soapenc:arrayType" wsdl:arrayType="xsd:short[]"/>
//        //    </restriction>
//        //   </complexContent>
//        //  </complexType>
//        if (attribute.hasAttribute("wsdl:arrayType")) {
//            String attributeName = attribute.getAttribute("ref");
//            String arrayType = attribute.getAttribute("wsdl:arrayType");
//            _elements.put(attributeName, _wsdlTypes.getTypeQName(arrayType));
//        }
//    }

    /**
     * Process a 'choice' element.
     *
     * @param choice
     */
    private void processChoice(Element choice) {
        // annotation?, (element | group | choice | sequence | any)*
        // just call process sequence, same elements
        processSequence(choice);
    }

    /**
     * Process a 'complexContent' element.
     *
     * @param complexContent
     */
    private void processComplexContent(Element complexContent) {
        // annotation?, (extension | restriction)
        Element child;
        if ((child = DomUtils.getChildElementByName(complexContent, "extension")) != null) {
            processComplexExtension(child);
        }
        else if ((child = DomUtils.getChildElementByName(complexContent, "restriction")) != null) {
            processComplexRestriction(child);
        }
    }

    /**
     * Process an 'extension' element whose parent is 'complexContent'.
     *
     * @param complexExtension
     */
    private void processComplexExtension(Element complexExtension) {
        // annotation?, (group, | all | choice | sequence)?, (attribute | attributeGroup)*, anyAttribute?
        Element child;
        if ((child = DomUtils.getChildElementByName(complexExtension, "group")) != null) {
            processGroup(child);
        }
        else if ((child = DomUtils.getChildElementByName(complexExtension, "all")) != null) {
            processAll(child);
        }
        else if ((child = DomUtils.getChildElementByName(complexExtension, "choice")) != null) {
            processChoice(child);
        }
        else if ((child = DomUtils.getChildElementByName(complexExtension, "sequence")) != null) {
            processSequence(child);
        }
    }

    /**
     * Process a 'restriction' element whose parent is 'complexContent'.
     *
     * @param complexRestriction
     */
    private void processComplexRestriction(Element complexRestriction) {
        // annotation?, (group | all | choice | sequence)?, (attribute | attributeGroup)*, anyAttribute?
        Element child;
        if ((child = DomUtils.getChildElementByName(complexRestriction, "group")) != null) {
            processGroup(child);
        }
        else if ((child = DomUtils.getChildElementByName(complexRestriction, "all")) != null) {
            processAll(child);
        }
        else if ((child = DomUtils.getChildElementByName(complexRestriction, "choice")) != null) {
            processChoice(child);
        }
        else if ((child = DomUtils.getChildElementByName(complexRestriction, "sequence")) != null) {
            processSequence(child);
        }
//        else if (DomUtils.getChildElementByName(complexRestriction, "attribute") != null) {
//            List<Element> attributes = DomUtils.getChildElementsByName(complexRestriction, "attribute");
//            for (Element attribute : attributes) {
//                processAttribute(attribute);
//            }
//        }
    }

    /**
     * Process an 'element'.
     *
     * @param element
     */
    private void processElement(Element element) {
        // annotation?
        if (element.hasAttribute("name")) {
            String elementName = element.getAttribute("name");
            String elementType = element.getAttribute("type");
            _elements.put(elementName.toLowerCase(), _wsdlTypes.getTypeQName(elementType));
            _elementNames.add(elementName);
        }
    }

    /**
     * Process a 'group' element.
     *
     * @param group
     */
    private void processGroup(Element group) {
        // annotation?, (all | choice | sequence)
        Element child;
        if ((child = DomUtils.getChildElementByName(group, "all")) != null) {
            processAll(child);
        }
        else if ((child = DomUtils.getChildElementByName(group, "choice")) != null) {
            processChoice(child);
        }
        else if ((child = DomUtils.getChildElementByName(group, "sequence")) != null) {
            processSequence(child);
        }
    }

    /**
     * Process a 'sequence' element.
     *
     * @param sequence
     */
    private void processSequence(Element sequence) {
        // annotation?, (element | group | choice | sequence | any)*
        List<Element> elements = DomUtils.getChildElementsByName(sequence, "element");
        for (Iterator<Element> itr = elements.iterator(); itr.hasNext();) {
            processElement((Element) itr.next());
        }

        elements = DomUtils.getChildElementsByName(sequence, "group");
        for (Iterator<Element> itr = elements.iterator(); itr.hasNext();) {
            processGroup((Element) itr.next());
        }

        elements = DomUtils.getChildElementsByName(sequence, "choice");
        for (Iterator<Element> itr = elements.iterator(); itr.hasNext();) {
            processChoice((Element) itr.next());
        }

        elements = DomUtils.getChildElementsByName(sequence, "sequence");
        for (Iterator<Element> itr = elements.iterator(); itr.hasNext();) {
            processSequence((Element) itr.next());
        }

        elements = DomUtils.getChildElementsByName(sequence, "any");
        for (Iterator<Element> itr = elements.iterator(); itr.hasNext();) {
            processAny((Element) itr.next());
        }
    }

    /**
     * Process a 'simpleContent' element.
     *
     * @param simpleContent
     */
    private void processSimpleContent(Element simpleContent) {
        // annotation?, (extension | restriction)
        Element child;
        if ((child = DomUtils.getChildElementByName(simpleContent, "extension")) != null) {
            processSimpleExtension(child);
        }
        else if ((child = DomUtils.getChildElementByName(simpleContent, "restriction")) != null) {
            processSimpleRestriction(child);
        }
    }

    /**
     * Process an 'extension' element whose parent is 'simpleContent'.
     *
     * @param any
     */
    private void processSimpleExtension(Element any) {
        // annotation?, (attribute | attributeGroup)*, anyAttribute?
        // *** noop ***
    }

    /**
     * Process a 'restriction' element whose parent is 'simpleContent'.
     *
     * @param simpleRestriction
     */
    private void processSimpleRestriction(Element simpleRestriction) {
        // annotation?, simpleType?, (enumeration | length | maxExclusive | maxInclusive
        // | maxLength | minExclusive | minInclusive | minLength | pattern | fractionDigits
        // | totalDigits | whiteSpace)*, (attribute | attributeGroup)*, anyAttribute?
        // *** noop ***
    }
}
